Fix rank-deficient matrix handling in OLS solver#91
Conversation
MultiPeriodDiD was producing astronomically wrong estimates (~252 trillion instead of ~2-5) due to rank-deficient design matrices being solved incorrectly by the gelsy LAPACK driver. Changes: - Python: Switch from gelsy to gelsd driver (SVD-based with truncation) - Rust: Replace least_squares() with explicit SVD + truncated pseudoinverse - Add comprehensive tests for rank-deficient matrices in both backends - Add Rust vs NumPy equivalence tests for rank-deficient cases - Document NaN standard errors limitation in TODO.md The gelsd driver properly handles rank-deficient matrices by truncating small singular values below rcond * max(S), producing valid minimum-norm solutions instead of garbage coefficients. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
|
Overall assessment:
Methodology
Code Quality
Performance
Maintainability
Tech Debt
Security
Documentation/Tests
|
…cation
Replace silent SVD truncation with R's lm() approach for rank-deficient matrices:
- Detect rank deficiency using pivoted QR decomposition
- Warn users with clear message listing dropped columns
- Set NaN for coefficients of linearly dependent columns
- Compute valid SEs for identified coefficients only
- Expand vcov matrix with NaN for dropped rows/columns
Add rank_deficient_action parameter ("warn", "error", "silent") to control behavior.
Hybrid Rust/Python routing:
- Full-rank matrices use fast Rust backend (when available)
- Rank-deficient matrices use Python backend for proper NA handling
(ndarray-linalg doesn't support QR with pivoting)
Also fixes tutorial notebook 02 to avoid rank deficiency by including
both treated cohorts in the MultiPeriodDiD example.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
|
/ai-review |
|
🔁 AI review rerun (requested by @igerber) Head SHA: Overall assessment: Executive Summary
Methodology
Code Quality
Performance
Maintainability
Tech Debt
Security
Documentation/Tests
|
Review feedback addressed: P1 - Methodology alignment: - Update REGISTRY.md to document R-style rank deficiency handling (warn + set NA instead of error) - Update TODO.md to mark NaN SE issue as resolved P2 - Tutorial fix: - Revert to single-cohort filter (cohort 3) for MultiPeriodDiD example - Add clear documentation about rank deficiency warning behavior - Explain that MultiPeriodDiD assumes simultaneous treatment timing P3 - Performance documentation: - Document QR decomposition overhead trade-off in code comments - Explain that ~2x cost is necessary for proper rank detection - Document why Python/Rust backends differ (pivoted QR not in ndarray-linalg) Note: vcov validation was already present in test_linalg.py (lines 242-251) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Instead of explaining rank deficiency behavior, create a clean dataset using generate_did_data() with simultaneous treatment timing. This shows MultiPeriodDiD working as intended without warnings or edge cases. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
|
/ai-review |
|
🔁 AI review rerun (requested by @igerber) Head SHA: Overall Assessment Executive Summary
Methodology
Code Quality
Performance
Maintainability
Tech Debt
Security
Documentation/Tests
|
P1 Fixes: - Add _factorize_cluster_ids() helper to handle string/categorical cluster IDs before passing to Rust backend (prevents type mismatch errors) - Add NaN check for treatment coefficient in TWFE to error early when treatment is perfectly collinear with fixed effects (clearer error message) - Fix LinearRegression.fit() to compute valid vcov when robust=False and matrix is rank-deficient (use reduced X matrix instead of full X) P2 Fixes: - Add skip_rank_check parameter to solve_ols() for performance-critical code paths where user knows matrix is full rank (avoids O(nk²) QR overhead) P3 Fixes: - Update fitted values and residuals documentation to clarify that for rank-deficient matrices, only identified coefficients are used Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
|
/ai-review |
|
🔁 AI review rerun (requested by @igerber) Head SHA: Overall assessment: Executive Summary
Methodology
Code Quality
Performance
Maintainability
Tech Debt
Security
Documentation/Tests
|
P1 Fixes: - Fix degrees of freedom calculation to use effective rank (n_params_effective_) instead of total columns when design matrix is rank-deficient. This ensures correct t-statistics, p-values, and confidence intervals for identified coefficients. - Improve Rust backend error message for rank-deficient X'X to suggest using solve_ols without skip_rank_check for R-style handling. - Improve TWFE collinearity error message to surface actual dropped column names and distinguish between treatment collinearity (error) vs covariate collinearity (warning). P2 Fixes: - Add inference validation tests that verify degrees of freedom, p-values, and confidence intervals are computed correctly when columns are dropped due to rank deficiency. Tests: - test_rank_deficient_degrees_of_freedom: Verifies n_params_effective_ and df_ - test_rank_deficient_inference_uses_correct_df: Verifies p-value and CI use correct df (n - rank) - test_rank_deficient_inference_nan_for_dropped_coef: Verifies NaN inference for dropped coefficients Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
|
/ai-review |
|
🔁 AI review rerun (requested by @igerber) Head SHA: Overall assessment: Executive Summary
Methodology
Code Quality
Performance
Maintainability
Tech Debt
Security
Documentation/Tests
|
P1 Fixes: - Update docs/methodology/REGISTRY.md to document rank-deficiency behavior for MultiPeriodDiD and TwoWayFixedEffects (edge cases section): - MultiPeriodDiD: warns and sets NA for dropped coefficients (R-style) - TWFE: treatment collinearity raises error, covariate collinearity warns - Fix Rust backend vcov for rank-deficient matrices: - Detect rank from SVD singular values (count values above threshold) - If rank < k, return NaN-filled vcov matrix instead of attempting X'X inversion - Updated docstring to document this behavior - Full R-style handling (QR pivoting) still requires Python backend Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
|
/ai-review |
|
🔁 AI review rerun (requested by @igerber) Head SHA: Overall assessment: Executive Summary
Methodology
Code Quality
Performance
Maintainability
Tech Debt
Security
Documentation/Tests
|
P2 Fixes: - Fix predict() to handle NaN coefficients by treating them as zero instead of producing all-NaN predictions. This matches the behavior of fitted values computed during fit(). P3 Fixes: - Fix adjusted R² to use n_params_effective_ (effective rank) instead of n_params_ (total columns) for consistency with df correction. Tests: - test_rank_deficient_predict_uses_identified_coefficients: Verifies predictions are finite and match fitted values for rank-deficient fits - test_rank_deficient_adjusted_r_squared_uses_effective_params: Verifies adjusted R² uses effective params, not total params Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
|
/ai-review |
|
🔁 AI review rerun (requested by @igerber) Head SHA: Overall Assessment: ✅ Looks good Executive Summary
Methodology
Code Quality
Performance
Maintainability
Tech Debt
Security
Documentation/Tests
|
- Add _skip_rank_check parameter to _solve_ols_numpy() for Python backend - Suppress duplicate collinearity warnings in TWFE (handles own messaging) - Add test_skip_rank_check_bypasses_qr_decomposition test - Add test_twfe_treatment_collinearity_raises_error test - Document QR+SVD redundancy rationale in TODO.md Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
|
/ai-review |
|
🔁 AI review rerun (requested by @igerber) Head SHA: Overall Assessment
Executive Summary
Methodology
Code Quality
Performance
Maintainability
Tech Debt
Security
Documentation/Tests
|
- Route MultiPeriodDiD through solve_ols with return_vcov=True and cluster_ids - Calculate degrees of freedom using effective rank (non-NaN coefficients) - Handle homoskedastic vcov case for rank-deficient matrices - Add test_rank_deficient_design_warns_and_sets_nan test This ensures MultiPeriodDiD properly handles rank-deficient design matrices by warning users, setting NaN for dropped coefficients, and computing valid SEs for identified coefficients only. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Added section explaining that check_finite=False doesn't fully bypass scipy's internal QR validation - Marked as low priority since it's an edge case optimization - QR+SVD redundancy was already documented in previous commit Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
|
/ai-review |
|
🔁 AI review rerun (requested by @igerber) Head SHA: Overall Assessment Executive Summary
Methodology
Code Quality
Performance
Maintainability
Tech Debt
Security
Documentation/Tests
|
- Change rcond from max(n,k)*eps to 1e-07 (matches R's qr() default) - Update both Python (_detect_rank_deficiency) and Rust (solve_ols) backends - Update REGISTRY.md to document the tolerance value - Fix test_near_singular_matrix_stability to use noise above new tolerance The previous tolerance (max(n,k)*eps ≈ 2.2e-14 for n=100) was stricter than R's default (1e-07), meaning near-collinear columns could be retained when R would drop them. This ensures consistent behavior with R's lm(). Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Update noise from 1e-8 to 1e-5 (above the 1e-07 rank detection threshold) to ensure the covariates are not treated as linearly dependent. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
|
/ai-review |
|
🔁 AI review rerun (requested by @igerber) Head SHA: Overall assessment: Executive summary
Methodology
Code Quality
Performance
Maintainability
Tech Debt
Security
Documentation/Tests
|
…ng (P3) P1: Average ATT now properly propagates NaN when: - All period effects are NaN (cannot compute average) - Vcov has NaN entries (dropped columns) - Only identified (non-NaN) effects are included in the average Previously, the code would set avg_t_stat=0 and avg_p_value from that, which could mislead inference when interaction terms were dropped. P3: Fix _detect_rank_deficiency docstring to document actual tolerance (1e-07 matching R's qr()) instead of the old max(n,k)*eps. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
|
/ai-review |
|
🔁 AI review rerun (requested by @igerber) Head SHA: Overall assessment: Executive summary
Methodology
Code Quality
Performance
Maintainability
Tech Debt
Security
Documentation/Tests
Open questions / assumptions
|
- avg_att is now NaN if ANY post-period effect is unidentified, matching R's default NA propagation semantics (mean(c(1,2,NA)) returns NA) - Add explicit cond=1e-07 to scipy_lstsq calls for consistency with Rust backend and QR rank tolerance - Document avg_att NA behavior in REGISTRY.md - Add test for avg_att NaN propagation when period effect is unidentified Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
|
/ai-review |
|
🔁 AI review rerun (requested by @igerber) Head SHA: Overall assessment: Executive summary
Methodology
Code Quality
Performance
Maintainability
Tech Debt
Security
Documentation/Tests
|
Add rank_deficient_action parameter to DifferenceInDifferences and LinearRegression classes, allowing users to control behavior when design matrices are rank-deficient: - "warn" (default): Issue warning and drop linearly dependent columns - "error": Raise ValueError - "silent": Drop columns silently without warning This fixes a documentation gap where REGISTRY.md claimed this parameter was controllable at the estimator level, but it was only available in the low-level solve_ols() function. Changes: - linalg.py: Add rank_deficient_action to LinearRegression.__init__ and pass through to solve_ols() in fit() - estimators.py: Add rank_deficient_action to DifferenceInDifferences and pass through to LinearRegression/solve_ols in fit() - MultiPeriodDiD inherits parameter and passes to solve_ols() - Add 8 new tests for the parameter in estimators and LinearRegression - Update CLAUDE.md with estimator-level usage examples Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
|
/ai-review |
|
🔁 AI review rerun (requested by @igerber) Head SHA: Overall Assessment
Executive Summary
Methodology
Code Quality
Performance
Maintainability
Tech Debt
Security
Documentation/Tests
|
Complete parameter propagation for rank_deficient_action across all estimators that use OLS internally: - TwoWayFixedEffects: Pass to LinearRegression, conditional warning suppression based on setting - CallawaySantAnna: Add parameter to __init__, pass to _linear_regression helper in _outcome_regression and _doubly_robust methods - SunAbraham: Add parameter to __init__, pass to LinearRegression in _fit_saturated_regression - TripleDifference: Add parameter to __init__, pass to solve_ols and LinearRegression in regression methods, also update convenience function All estimators now support: - "warn" (default): Issue warning and drop linearly dependent columns - "error": Raise ValueError on rank deficiency - "silent": Drop columns silently without warning Tests added for each estimator verifying error/silent behavior. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
|
/ai-review |
|
🔁 AI review rerun (requested by @igerber) Head SHA: Overall assessmentExecutive summary
Methodology
Code Quality
Performance
Maintainability
Tech Debt
Security
Documentation/Tests
|
P1 - Methodology Documentation: - Document rank-deficiency handling for CallawaySantAnna in REGISTRY.md - Document rank-deficiency handling for SunAbraham in REGISTRY.md - Match format used for DifferenceInDifferences section P2 - Rust NaN vcov Fallback: - Add check in solve_ols() for NaN in vcov after Rust backend call - Fall back to Python R-style handling when QR/SVD disagree on rank - Prevents silent NaN SEs from reaching users - Add test_nan_vcov_fallback_to_python() test case Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
|
/ai-review |
|
🔁 AI review rerun (requested by @igerber) Head SHA: Overall assessment: Executive Summary
Methodology
Code Quality
Performance
Maintainability
Tech Debt
Security
Documentation/Tests
|
- Add rank_deficient_action to DifferenceInDifferences.get_params() (fixes sklearn-style cloning/serialization for DiD, TWFE, MultiPeriodDiD) - Fix NaN vcov fallback to force fresh QR rank detection instead of passing cached rank info that may have incorrectly assessed full-rank - Strengthen test_nan_vcov_fallback_to_python to verify R-style NaN handling actually occurs when fallback is triggered Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
|
/ai-review |
|
🔁 AI review rerun (requested by @igerber) Head SHA: Overall assessment: ✅ Looks good Executive Summary
Methodology
Code Quality
Performance
Maintainability
Tech Debt
Security
Documentation/Tests
|
Add estimator inheritance map, test writing guidelines, and development checklists based on lessons learned from PR #91 review process. These additions help prevent common issues: - Estimator inheritance map: Shows which classes inherit get_params/set_params to prevent missing parameters in subclasses - Test writing guidelines: Emphasizes testing behavior (not just no-crash) for fallback paths, new parameters, and warnings - Development checklists: Actionable steps for adding parameters and warning/error handling with verification steps Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Summary
gelsytogelsdLAPACK driver (SVD-based with proper singular value truncation)least_squares()with explicit SVD + truncated pseudoinverse matching scipy's behaviorMethodology references (required if estimator / math changes)
Validation
tests/test_linalg.py: Updatedtest_rank_deficient_still_solves, addedtest_multiperiod_like_rank_deficiencytests/test_rust_backend.py: Addedtest_rank_deficient_matrix_produces_valid_coefficients,test_multiperiod_did_like_design_matrix,test_rank_deficient_ols_match,test_multiperiod_did_design_equivalenceSecurity / privacy
Generated with Claude Code